package cz.zcu.fav.rat.dataaccess.service.impl;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.joda.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import cz.zcu.fav.rat.beans.Period;
import cz.zcu.fav.rat.beans.PickAction;
import cz.zcu.fav.rat.beans.Reservation;
import cz.zcu.fav.rat.dataaccess.dao.PeriodDAO;
import cz.zcu.fav.rat.dataaccess.dao.ReservationDAO;
import cz.zcu.fav.rat.dataaccess.dao.UserDAO;
import cz.zcu.fav.rat.dataaccess.service.SettingsFactory;
import cz.zcu.fav.rat.dataaccess.service.ReservationService;
import cz.zcu.fav.rat.dataaccess.service.TimeSlot;

@Service("reservationService")
@Transactional
public class ReservationServiceImpl extends
		GenericDataServiceImpl<Reservation, ReservationDAO> implements
		ReservationService
{
	@Autowired
	PeriodDAO periodDAO;

	@Autowired
	UserDAO userDao;

	@Autowired
	SettingsFactory settingsFactory;

	@Autowired
	public ReservationServiceImpl(ReservationDAO dao)
	{
		super(dao);
	}

	@Transactional(readOnly = false, rollbackFor = OccupiedReservationTimeSlotException.class)
	public Set<Reservation> createReservations(Set<Reservation> reservations)
			throws OccupiedReservationTimeSlotException
	{
		Set<Reservation> evaluatedReservations = new HashSet<Reservation>();

		for (Reservation reservation : reservations)
		{
			create(reservation);
			evaluatedReservations.add(reservation);
		}

		return evaluatedReservations;
	}

	private void checkReservation(Reservation reservation)
			throws OccupiedReservationTimeSlotException
	{
		for (Period period : reservation.getPeriods())
		{
			LocalDateTime from = period.getFromDate();
			LocalDateTime to = period.getToDate();
			Duration duration = new Interval(from.toDateTime(), to.toDateTime())
					.toDuration();
			Duration maxDuration = settingsFactory
					.getMaximumReservationDuration();

			if (duration.isShorterThan(settingsFactory
					.getMinimumReservationDuration()))
			{
				period.setToDate(from.plus(settingsFactory
						.getMinimumReservationDuration()));
			} else if (duration.isLongerThan(maxDuration))
			{
				period.setToDate(from.plus(maxDuration));
			}

			List<Period> periods = periodDAO
					.findValidPeriodsByObjectIdInDateRange(reservation
							.getObject().getId(), from, from, from, period
							.getToDate());

			if (!periods.isEmpty())
			{
				for (Period per : periods)
				{
					// certainly user can modify his own reservation, thats
					// perfectly ok
					if (!per.getReservation().getId()
							.equals(reservation.getId()))
					{
						// now we must check expirations
						if (per.getPickAction() == PickAction.NOT_PICKED
								&& per
										.getFromDate()
										.plus(settingsFactory
												.getReservationExpirationLimit())
										.isBefore(LocalDateTime.now()))
						{
							//clean up period as it is placed after expiration limit
							per.setValid(false);
							periodDAO.update(per);
						} else
						{
							throw new OccupiedReservationTimeSlotException(
									per.getId());
						}
					}
				}
			}
		}
	}

	@Override
	@Transactional(readOnly = false, rollbackFor = OccupiedReservationTimeSlotException.class)
	public Integer create(Reservation newInstance)
			throws OccupiedReservationTimeSlotException
	{
		movePeriodsToNow(newInstance);
		checkReservation(newInstance);
		return super.create(newInstance);
	}

	@Transactional(readOnly = true)
	private Period checkTimeSlots(List<TimeSlot> timeslots, Integer objectId)
	{
		Period per = null;
		for (TimeSlot sl : timeslots)
		{
			per = checkTimeSlot(sl, objectId);
			if (per != null)
			{
				break;
			}
		}
		return per;
	}

	@Transactional(readOnly = true)
	private Period checkTimeSlot(TimeSlot timeslot, Integer objectId)
	{
		List<Period> periods = periodDAO.findValidPeriodsByObjectIdInDateRange(
				objectId, timeslot.getSince(), timeslot.getSince(),
				timeslot.getSince(), timeslot.getTo());
		if (!periods.isEmpty())
		{
			return periods.iterator().next();
		}
		return null;
	}

	private void movePeriodsToNow(Reservation reservation)
	{

		for (Period period : reservation.getPeriods())
		{
			if (period.getFromDate().isBefore(LocalDateTime.now()))
			{

				Duration duration = new Duration(period.getFromDate()
						.toDateTime(), DateTime.now());
				for (Period modifiedPeriod : reservation.getPeriods())
				{
					modifiedPeriod.setFromDate(modifiedPeriod.getFromDate()
							.plus(duration));
					modifiedPeriod.setToDate(modifiedPeriod.getToDate().plus(
							duration));
				}
			}
		}

	}

	@Override
	@Transactional(readOnly = false, rollbackFor = OccupiedReservationTimeSlotException.class)
	public Reservation mergeMultiple(Period period, TimeSlot timeslot)
			throws OccupiedReservationTimeSlotException
	{

		Period oldPeriod = period;
		Reservation reservation = period.getReservation();
		DateTime oldSince = oldPeriod.getFromDate().toDateTime();
		DateTime oldTo = oldPeriod.getToDate().toDateTime();
		DateTime newSince = timeslot.getSince().toDateTime();
		DateTime newTo = timeslot.getTo().toDateTime();

		Duration sinceTimeChange = null;
		Duration toTimeChange = null;

		boolean plusTimeSince;
		boolean plusTimeto;

		if (oldSince.isAfter(newSince))
		{
			sinceTimeChange = new Duration(newSince, oldSince);
			plusTimeSince = false;
		} else
		{
			sinceTimeChange = new Duration(oldSince, newSince);
			plusTimeSince = true;
		}

		if (oldTo.isAfter(newTo))
		{
			toTimeChange = new Duration(newTo, oldTo);
			plusTimeto = false;
		} else
		{
			toTimeChange = new Duration(oldTo, newTo);
			plusTimeto = true;
		}

		for (Period per : reservation.getPeriods())
		{
			// DO not modify any periods in the past
			if (!(per.getFromDate().isBefore(LocalDateTime.now()) && per
					.getToDate().isBefore(LocalDateTime.now())))
			{
				per.setFromDate(plusTimeSince ? per.getFromDate().plus(
						sinceTimeChange) : per.getFromDate().minus(
						sinceTimeChange));

				per.setToDate(plusTimeto ? per.getToDate().plus(toTimeChange)
						: per.getToDate().minus(toTimeChange));
			}
		}

		checkReservation(reservation);

		reservation = merge(reservation);
		// update(reservation);
		return reservation;
	}

	@Override
	public List<Reservation> getAllReservationsByUserId(Integer userId)
	{
		return dao.findAllReservationsByUserId(userId);
	}
}
